import bpy
from typing import List
from bpy.props import BoolProperty, EnumProperty, StringProperty
from collections import defaultdict
from ...addon.naming import FluidLabNaming
from bpy.types import Context, Object, Event, Operator
from ...libs.functions.basics import ocultar_post_panel_settings, set_active_object, append_function_unique
from ...libs.functions.get_common_vars import get_common_vars
from ..lists.op_list_reuse_base import FLUIDLAB_OT_list_reuse_base
from ...properties.lists import FluidGroupList
import datetime


def get_time_now(start):

    total_time = datetime.datetime.now() - start
    
    segundos = total_time.total_seconds()
    horas = segundos // 3600
    minutos = (segundos % 3600) // 60
    segundos = segundos % 60
    milisegundos = total_time.microseconds // 1000
    
    if horas >= 1:
        return f"{int(horas)}h {int(minutos)}m {int(segundos)}s {milisegundos}ms"
    elif minutos >= 1:
        return f"{int(minutos)}m {int(segundos)}s {milisegundos}ms"
    elif segundos >= 1:
        return f"{int(segundos)}s {milisegundos}ms"
    else:
        return f"{milisegundos}ms"
    


class FLUIDLAB_OT_popup_info_report(Operator):
    bl_idname = "fluidlab.popup_info_report"
    bl_label = "Report"
    bl_description = "Report Class"
    # bl_options = {"REGISTER", "UNDO"}

    report_txt: StringProperty(default="")

    def execute(self, context: Context):
        return {'FINISHED'}
    
    def invoke(self, context, event):
        wm = context.window_manager
        return wm.invoke_popup(self)

    def draw(self, context):
        layout = self.layout
        layout.label(text=self.report_txt, icon='INFO')


class FLUIDLAB_OT_bake_current_cache(FLUIDLAB_OT_list_reuse_base):
    bl_idname = "fluidlab.bake_current_cache"
    bl_label = "Bake Current Cache"
    bl_description = "Bake Current Cache"
    bl_options = {"REGISTER", "UNDO"}

    # Blender properties:
    use_disk_cache: BoolProperty(default=False)
    compression: EnumProperty(
        items=[
            ('NO',      "None",     "", 0),
            ('LIGHT',   "Light",    "", 1),
            ('HEAVY',   "Heavy",    "", 2),
        ],
        default='NO'
    )

    # Object properties:
    other_objects = []
    all_psys = {}

    time_now = None

    def invoke(self, context:Context, event:Event):
        # Guardo los index previos que tuviera el usuario:
        fluid_groups = get_common_vars(context, get_fluid_groups=True)
        active_group = fluid_groups.active
        emitters_list = active_group.emitters
        self.prev_group_index = fluid_groups.list_index
        self.prev_emitter_index = emitters_list.list_index

        wm = context.window_manager
        wm[FluidLabNaming.WM_POPUP_OPENED] = True

        # Este metodo es todo igual que el de FLUIDLAB_OT_list_reuse_base pero tuve que ponerle lo siguiente:
        wm[FluidLabNaming.WM_BAKE_POPUP_OPENED] = True
        return context.window_manager.invoke_props_dialog(self, width=300)

    
    def draw(self, context):

        layout = self.layout
        layout.use_property_decorate = False
        layout.use_property_split = False

        fluid_groups = get_common_vars(context, get_fluid_groups=True)
        active_group = fluid_groups.active

        if active_group:
            is_saved = bpy.data.is_saved
        
            listados = layout.row(align=True)
            listados.template_list("FLUIDLAB_UL_draw_fluids_groups", "", fluid_groups, "list", fluid_groups, "list_index", rows=3)
        
            fluid_emitters = active_group.emitters
            listados.template_list("FLUIDLAB_UL_draw_fluids_emitters", "", fluid_emitters, "list", fluid_emitters, "list_index", rows=3)
            
            use_disk_cache = layout.row(align=True)

            use_disk_cache_l = use_disk_cache.row(align=True)
            use_disk_cache_l.prop(self, "use_disk_cache", text="Disk Cache")
            use_disk_cache_l.enabled = is_saved
            compression = use_disk_cache_l.row(align=True)
            compression.prop(self, "compression", text="")
            compression.enabled = self.use_disk_cache
            if not is_saved:
                use_disk_cache_r = layout.row(align=True)
                use_disk_cache_r.box().label(text="To use 'Disk Cache' option, save your scene first.", icon='INFO')

    
    def get_ob_to_work(self, fluid_groups:FluidGroupList) -> List[Object]:
        
        # Todos los grupos:
        all_groups = fluid_groups.get_all_items

        # Guardo los objetos Target:
        all_valid_emitters = set()
        valid_groups = [group for group in all_groups if group.bakeme]

        for group in valid_groups:
            emitters_list = group.emitters
            valid_emitters = emitters_list.get_all_bakeable_emitters
            all_valid_emitters.update(valid_emitters)

        # Guardo el resto de objetos que no son el target, (para apagar sus modifiers y evitar ser cacheados):
        other_objects = set()
        for group in all_groups:
            others = [emitter_item.emitter for emitter_item in group.emitters.get_all_items if not emitter_item.bakeme and hasattr(emitter_item, "emitter")]
            other_objects.update(others)

        # Convertimos los conjuntos en un listados:
        ob_to_work = list(all_valid_emitters)
        self.other_objects = list(other_objects)

        return ob_to_work
    

    def other_obs_modifiers_switch(self, new_status:bool) -> None:

        """ Apagamos en el resto de objetos, el modifier de particulas para evitar ser cacheados """

        for ob in self.other_objects:
            
            mod_modes = []

            for i, mod in enumerate(ob.modifiers):
                                
                if not mod.name.startswith(FluidLabNaming.PS_NAME):
                    continue

                if new_status == False:
                    # si es False, guardo los estados previos:
                    mod_modes.append(mod.show_viewport)
                    mod.show_viewport = False
                else:
                    # si es True restauro el valor previo:
                    if FluidLabNaming.PREV_STATUS_PS_MOD in ob:
                        mod.show_viewport = ob[FluidLabNaming.PREV_STATUS_PS_MOD][i]

            if new_status == False:
            
                # si es false guardo los estados previos:
                ob[FluidLabNaming.PREV_STATUS_PS_MOD] = mod_modes
            
            else: # si es ture ya lo habré restaurado:
            
                if FluidLabNaming.PREV_STATUS_PS_MOD in ob:
                    del ob[FluidLabNaming.PREV_STATUS_PS_MOD]

    def get_psys_and_set_use_disk(self, ob_to_work:List[Object]) -> None:
        
        all_psys = defaultdict(list)

        for ob in ob_to_work:
            for psys in ob.particle_systems:

                # Trabajamos solo con nuestras particulas:
                if psys.settings.fluidlab.id_name == "":
                    continue

                # Si ya esta bakeado paso de el:
                cache = psys.point_cache
                if cache.is_baked:
                    continue

                # El usuario elige si lo escupe al disco:
                psys.point_cache.use_disk_cache = self.use_disk_cache
                psys.point_cache.compression = self.compression
                all_psys[ob].append(psys)
        
        self.all_psys = all_psys
    
    
    def bake_cc(self, context, scn) -> None:
        if self.all_psys:

            for ob, psys_list in self.all_psys.items():

                for psys in psys_list:                
                    cache = psys.point_cache
                
                    with context.temp_override(scn=scn, active_object=ob, point_cache=cache):
                        bpy.ops.ptcache.bake_from_cache()
    

    
    def play_timeline(self, scn, context) -> None:
    
        def stop_animation(uno, dos) -> None:

            # Detener la animación
            bpy.ops.screen.animation_cancel(restore_frame=False)

            # rebobinamos al principio:
            scn.frame_set(scn.frame_start)

            # Hacemos el current cache:
            self.bake_cc(context, scn)

            # Volvemos a restaurar los estados de los modifiers de los otros objetos (los no targets):
            if self.other_objects:
                self.other_obs_modifiers_switch(True)
        
        def stoped_animation(context) -> None:
            # Si ya no esta en play (deduzco q se hizo ESC)
            if not context.screen.is_animation_playing:
                # Detener la animación:
                stop_animation(None, None) # <-- aunque ya este detenida, ejecutamos las cosas finales igualmente (bake_cc, y restore mods)
                if stoped_animation in bpy.app.handlers.frame_change_post:
                    bpy.app.handlers.frame_change_post.remove(stoped_animation)


        def check_end_of_animation(scn, context) -> None:
            if scn.frame_current >= scn.frame_end:
                
                # Detener la animación al llegar al final
                stop_animation(None, None)
                
                time_bake = get_time_now(self.time_now)
                
                if stoped_animation in bpy.app.handlers.frame_change_post:
                    bpy.app.handlers.frame_change_post.remove(stoped_animation)
                    scn.fluidlab.last_bake_time = time_bake
                    # bpy.ops.fluidlab.popup_info_report('INVOKE_DEFAULT', report_txt="The bake was made in %.3f seconds" % (time() - self.time_now))
                    # self.report({'INFO'}, "The bake was made in %.3f seconds" % (time() - self.time_now))
                    # print("[INFO]: -> fluidlab.bake_all %.3f seconds" % (time() - self.time_now))

                if check_end_of_animation in bpy.app.handlers.frame_change_post:
                    bpy.app.handlers.frame_change_post.remove(check_end_of_animation)
                    scn.fluidlab.last_bake_time = time_bake
                    # bpy.ops.fluidlab.popup_info_report('INVOKE_DEFAULT', report_txt="The bake was made in %.3f seconds" % (time() - self.time_now))
                    # self.report({'INFO'}, "The bake was made in %.3f seconds" % (time() - self.time_now))
                    # print("[INFO]: -> fluidlab.bake_all %.3f seconds" % (time() - self.time_now))


        # Rebobinamos al principio
        scn.frame_set(scn.frame_start)

        # Agregar el manejador para verificar si ya no esta en play:
        # append_function_unique(bpy.app.handlers.frame_change_post, stop_animation)
        # bpy.app.handlers.frame_change_post.append(stoped_animation)

        # Agregar el manejador para verificar el final de la animación
        append_function_unique(bpy.app.handlers.frame_change_post, check_end_of_animation)
        # bpy.app.handlers.frame_change_post.append(check_end_of_animation)
        
        # Reproducir la animación
        bpy.ops.screen.animation_play()
        
    
    

    def execute(self, context:Context):
        self.time_now = datetime.datetime.now()

        scn, fluid_groups = get_common_vars(context, get_scn=True, get_fluid_groups=True)

        # Obtenemos los emitters que se les va a hacer el bake:
        ob_to_work = self.get_ob_to_work(fluid_groups)
        if not ob_to_work:
            self.report({'ERROR'}, f"Cant get any object to work!")
            return {'CANCELLED'}
        
        # Apagamos los modifiers de los otros objetos (para q no los cachee):
        if self.other_objects:
            self.other_obs_modifiers_switch(False)
        
        # Primero tengo que setear si usara el disk cache o no, porque sino lo considera un cambio.
        self.get_psys_and_set_use_disk(ob_to_work)

        # Hacemos play para luego hacer el current cache:
        self.play_timeline(scn, context) # <-- hacemos en los eventos el stop animation, el bake_cc y el restore modifiers status

        # Restauro los index (Fluid Group y Emitters list) previos que tuviera el usuario:
        self.restore_indexs(context)
        ocultar_post_panel_settings()
        return {'FINISHED'}


class FLUIDLAB_OT_bake_refresh_cache(Operator):
    bl_idname = "fluidlab.bake_refresh_cache"
    bl_label = "Refresh Cache"
    bl_description = "Refresh Cache"
    bl_options = {"REGISTER", "UNDO"}


    def draw(self, context:Context) -> None:

        layout = self.layout
        layout.use_property_decorate = False
        layout.use_property_split = False

        fluid_groups = get_common_vars(context, get_fluid_groups=True)
        active_group = fluid_groups.active

        if active_group:
        
            listados = layout.row(align=True)
            listados.template_list("FLUIDLAB_UL_draw_fluids_groups", "", fluid_groups, "list", fluid_groups, "list_index", rows=3)
        
            fluid_emitters = active_group.emitters
            listados.template_list("FLUIDLAB_UL_draw_fluids_emitters", "", fluid_emitters, "list", fluid_emitters, "list_index", rows=3)


    def execute(self, context):

        scn, fluid_groups = get_common_vars(context, get_scn=True, get_fluid_groups=True)
        all_groups = fluid_groups.get_all_items
        
        if not all_groups:
            print("Cant get all groups!")
            return {'CANCELLED'}

        # rebobinamos al principio:
        scn.frame_set(scn.frame_start)

        # Obtengo todos los emisores de todos los grupos para contar el total de particulas de todos los grupos:                            
        all_emitters = [group.emitters.get_all_emitters for group in all_groups]
        
        # Aplanando en una sola lista todos los emittes:
        all_emitters = [ob for sublista in all_emitters for ob in sublista]

        # solo los que tengan el modifier target:
        all_emitters = [ob for ob in all_emitters if ob.modifiers.get(FluidLabNaming.PS_NAME)]

        # Guardo los que estaban previamente apagados, para restaurar su estado al terminar:
        prevously_off = [ob for ob in all_emitters if ob.modifiers[FluidLabNaming.PS_NAME].show_viewport == False]

        # Reseteamos las caches a todos los emitters:
        for emitter_ob in all_emitters:

            # Si estaba apagado pasamos de el:
            # if emitter_ob in prevously_off:
            #     continue

            ps_mod = emitter_ob.modifiers.get(FluidLabNaming.PS_NAME)
            
            # apagando y encendiendo el modifier:
            ps_mod.show_viewport = False
            ps_mod.show_viewport = True

            # si estaba previamente apagado lo volvemos a dejar como estaba:
            if emitter_ob in prevously_off:
                ps_mod.show_viewport = False

            # for psys in emitter_ob.particle_systems:
                
            #     # Si no tiene el nombre de Fluidlab o si ya esta bakeado pasamos de el:
            #     if not psys.name.startswith(FluidLabNaming.PS_NAME) or psys.point_cache.is_baked:
            #         continue
                
            #     # Reseteo su cache:
            #     # prev_lifetime = psys.settings.lifetime
            #     # psys.settings.lifetime = prev_lifetime + 1
            #     # psys.settings.lifetime = prev_lifetime
            #     psys.seed = random.randint(2, 100)

        # scn.frame_set(scn.frame_start+1)
        # scn.frame_set(scn.frame_start)

        # Para dar feedback:
        if "current_cache_baked" in scn.fluidlab:
            del scn.fluidlab["current_cache_baked"]
                
        ocultar_post_panel_settings()
        return {'FINISHED'}


class FLUIDLAB_OT_current_cache_to_bake(Operator):
    bl_idname = "fluidlab.current_cache_to_bake"
    bl_label = "Current Cache To Bake"
    bl_description = "Current Cache To Bake"
    bl_options = {"REGISTER", "UNDO"}

    """ Este ejecuta el de blender pero no permite ponerlo en mi ui sin estar en gris """

    def execute(self, context):
        
        scn, fluid_groups = get_common_vars(context, get_scn=True, get_fluid_groups=True)

        all_groups = fluid_groups.get_all_items

        for group in all_groups:

            emitters_list = group.emitters
            all_emitters = emitters_list.get_all_emitters

            for emitter_ob in all_emitters:
                                
                for psys in emitter_ob.particle_systems:
                
                    cache = psys.point_cache
                    
                    # Si no son nuestras particulas, o ya estan bakeadas, skipeamos:
                    if psys.settings.fluidlab.id_name == "" or cache.is_baked:
                        continue
                    
                    # Sobreescribimos el contexto para poder ejecutar Current Cache to Bake de blender:
                    with context.temp_override(scn=scn, active_object=emitter_ob, point_cache=cache):
                        bpy.ops.ptcache.bake_from_cache()

                        # Para dar feedback:
                        if "current_cache_baked" not in scn.fluidlab:
                            scn.fluidlab["current_cache_baked"] = True

        
        return {'FINISHED'}



class FLUIDLAB_OT_simulation_nodes_cache_bake(Operator):
    bl_idname = "fluidlab.simulation_nodes_cache_bake"
    bl_label = "Bake Simulation Cache"
    bl_description = "Bake simulations in geometry nodes modifiers"
    bl_options = {"REGISTER", "UNDO"}

    selected: BoolProperty(default=True)

    def execute(self, context):
        
        fluid_mesh = get_common_vars(context, get_fluid_mesh=True)

        prev_selection = context.selected_objects
        prev_activ_ob = context.active_object

        bpy.ops.object.select_all(action='DESELECT')

        mesh_item_active = fluid_mesh.active
        ob = mesh_item_active.ob

        ob.select_set(True)
        set_active_object(context, ob)
        bpy.ops.object.simulation_nodes_cache_bake(selected=self.selected)

        bpy.ops.object.select_all(action='DESELECT')

        if prev_selection:
            for ob in prev_selection:
                ob.select_set(True)

        if prev_activ_ob:
            set_active_object(context, prev_activ_ob)
        
        return {'FINISHED'}


class FLUIDLAB_OT_simulation_nodes_cache_delete(Operator):
    bl_idname = "fluidlab.simulation_nodes_cache_delete"
    bl_label = "Delete Cached Simulation"
    bl_description = "Delete cached/baked simulations in geometry nodes modifiers"
    bl_options = {"REGISTER", "UNDO"}

    selected: BoolProperty(default=True)

    def execute(self, context):
        
        fluid_mesh = get_common_vars(context, get_fluid_mesh=True)

        prev_selection = context.selected_objects
        prev_activ_ob = context.active_object

        bpy.ops.object.select_all(action='DESELECT')

        mesh_item_active = fluid_mesh.active
        ob = mesh_item_active.ob

        ob.select_set(True)
        set_active_object(context, ob)
        bpy.ops.object.simulation_nodes_cache_delete(selected=self.selected)

        bpy.ops.object.select_all(action='DESELECT')

        if prev_selection:
            for ob in prev_selection:
                ob.select_set(True)

        if prev_activ_ob:
            set_active_object(context, prev_activ_ob)
        
        return {'FINISHED'}

class FLUIDLAB_OT_my_bake_all(Operator):
    bl_idname = "fluidlab.bake_all"
    bl_label = "Bake All"
    bl_description = "Bake All"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        start = datetime.datetime.now()

        bpy.ops.fluidlab.bake_refresh_cache()
        bpy.ops.ptcache.bake_all()
        # print("[INFO]: -> fluidlab.bake_all %.3f seconds" % (time() - time_now))
        
        time_bake = get_time_now(start)
        context.scene.fluidlab.last_bake_time = time_bake

        self.report({'INFO'}, f"The bake was made in {time_bake}")
        # este aquí no me lo muestra no se porque:
        # bpy.ops.fluidlab.popup_info_report('INVOKE_DEFAULT', report_txt="The bake was made in %.3f seconds" % (time() - time_now))
        return {'FINISHED'}


# Start Simulation y Sopt Simulation:


# class FLUIDLAB_OT_bake_start_simulation(Operator):
#     bl_idname = "fluidlab.bake_start_simulation"
#     bl_label = "Start Simulation"
#     bl_description = "Start Simulation"
#     bl_options = {"REGISTER", "UNDO"}

#     def execute(self, context):

#         scn, fluid_groups = get_common_vars(context, get_scn=True, get_fluid_groups=True)
#         wm = context.window_manager

#         all_groups = fluid_groups.get_all_items
#         if not all_groups:
#             print("Cant get all groups!")
#             return {'CANCELLED'}

#         # Obtengo todos los emisores de todos los grupos para contar el total de particulas de todos los grupos:                            
#         all_emitters = [group.emitters.get_all_emitters for group in all_groups]
        
#         # Aplanando en una sola lista todos los emittes:
#         all_emitters = [objeto for sublista in all_emitters for objeto in sublista]


#         for emitter_ob in all_emitters:
#             for psys in emitter_ob.particle_systems:
                
#                 if not psys.name.startswith(FluidLabNaming.PS_NAME):
#                     continue

#                 # Si ya esta bakeado paso de el:
#                 cache = psys.point_cache
#                 if cache.is_baked:
#                     continue
                
#                 # Reseteo su cache:
#                 prev_fend = psys.settings.frame_end
#                 psys.settings.frame_start += 1
#                 psys.settings.frame_start -= 1
#                 psys.settings.frame_end = prev_fend

#         # rebobinamos al principio:
#         scn.frame_set(scn.frame_start)

#         bpy.ops.screen.animation_play()
        
#         wm[FluidLabNaming.SS_SRT_STP] = True

#         return {'FINISHED'}


# class FLUIDLAB_OT_bake_stop_simulation(Operator):
#     bl_idname = "fluidlab.bake_stop_simulation"
#     bl_label = "Stop Simulation"
#     bl_description = "Stop Simulation"
#     bl_options = {"REGISTER", "UNDO"}

#     def execute(self, context):
        
#         scn = get_common_vars(context, get_scn=True)
#         wm = context.window_manager
        
#         if context.screen.is_animation_playing:
#             bpy.ops.screen.animation_cancel()

#         # rebobinamos al principio:
#         scn.frame_set(scn.frame_start)

#         if FluidLabNaming.SS_SRT_STP in wm:
#             del wm[FluidLabNaming.SS_SRT_STP]
        
#         return {'FINISHED'}


class FLUIDLAB_OT_bake_ptcache_free_bake_all(Operator):
    bl_idname = "fluidlab.bake_ptcache_free_bake_all"
    bl_label = "Free Bake All"
    bl_description = "Free Bake All"
    bl_options = {"REGISTER", "UNDO"}


    def execute(self, context):
        
        scn, fluidlab, fluid_groups = get_common_vars(context, get_scn=True, get_fluidlab=True, get_fluid_groups=True)
        
        all_groups = fluid_groups.get_all_items
        if not all_groups:
            print("Cant get all groups!")
            return {'CANCELLED'}
        
        #-------------------------------------------------------------------------
        # Desactivamos todos los unborn de todos los emitters de todos los grupos:
        #-------------------------------------------------------------------------

        # Obtengo todos los emisores de todos los grupos para contar el total de particulas de todos los grupos:                            
        all_emitters_items = [group.emitters.get_all_items for group in all_groups]
        
        # Aplanando en una sola lista todos los emittes_items:
        all_emitters_items = [objeto for sublista in all_emitters_items for objeto in sublista]

        # # Reseteamos las caches a todos los emitters:
        # for emitter_item in all_emitters_items:
        #     emitter_item.emission.show_unborn = False

        # limpiamos la cache:
        # bpy.ops.ptcache.free_bake_all() # ESTE BORRA LOS BAKES DE LOS RIGIDBODIES POR ESO NO NOS VALE

        for emitter_item in all_emitters_items:
            
            emitter_ob = next((ob for ob in emitter_item.group_coll.objects if ob.fluidlab.id_name == "Emitter_"+ emitter_item.id_name ), None)

            # por todos sus sistemas de particulas:
            for psys in emitter_ob.particle_systems:
                
                # Solo trabajamos con nuestras particulas:
                if psys.settings.fluidlab.id_name == "":
                    continue

                with context.temp_override(object=emitter_ob, point_cache=psys.point_cache):
                    bpy.ops.ptcache.free_bake()

                    # Para dar feedback:
                    # if "current_cache_baked" in scn.fluidlab:
                    #     del scn.fluidlab["current_cache_baked"]

        fluidlab.last_bake_time = ""
        return {'FINISHED'}
